﻿// <copyright file="BezierVisual.cs" company="Oleg V. Polikarpotchkin">
// Copyright © 2008 Oleg V. Polikarpotchkin. All Right Reserved
// </copyright>
// <author>Oleg V. Polikarpotchkin</author>
// <email>ov-p@yandex.ru</email>
// <date>2008-12-23</date>
// <summary>OpenWPFChart library. Bezier Sampled Curve Visual.</summary>
// <revision>$Id$</revision>

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Media;

namespace OpenWPFChart.Parts
{
	/// <summary>
	/// Bezier Sampled Curve visual class draws the curve passed in by Points data
	/// as poly Bezier curve.
	/// </summary>
	/// <remarks>
	/// <para>If PointMarkerGeometry property is set the data point markers are drawn.</para>
	/// </remarks>
	[Interpolated("Bezier")]
	public class BezierVisual : ItemVisual, ICurveVisual
	{
		/// <summary>
		/// Renders the Curve.
		/// </summary>
		protected internal override void Render()
		{
			Children.Clear();

			using (DrawingContext dc = RenderOpen())
			{
				SampledCurveDataView dataView = DataView as SampledCurveDataView;
				if (dataView == null)
					return;
				IPoint2DSeries pointSeries = dataView.ItemData as IPoint2DSeries;
				if (pointSeries == null)
					return;

				ChartScale hScale = dataView.HorizontalScale, vScale = dataView.VerticalScale;
				if (hScale == null || !hScale.CompatibleWith(dataView.ItemData.GetDimensionType(0))
					|| vScale == null || !vScale.CompatibleWith(dataView.ItemData.GetDimensionType(1)))
					return;

				// Points to draw.
				IEnumerable<Point> points = null;
				if (dataView.Orientation == Orientation.Horizontal)
					points = from pt in pointSeries.Points 
							 select new Point(hScale.ToPixels(pt.X), vScale.ToPixels(pt.Y));
				else // Orientation == Orientation.Vertical
					points = from pt in pointSeries.Points 
							 select new Point(vScale.ToPixels(pt.Y), hScale.ToPixels(pt.X));
				if (points.Count() == 0)
					return; // Nothing to draw

				// Chart area size.
				Size areaSize;
				if (dataView.Orientation == Orientation.Horizontal)
					areaSize = new Size(hScale.ToPixels(hScale.Stop), vScale.ToPixels(vScale.Stop));
				else
					areaSize = new Size(vScale.ToPixels(vScale.Stop), hScale.ToPixels(hScale.Stop));

				// Clipping region.
				RectangleGeometry clip = new RectangleGeometry(new Rect(0, 0
						, areaSize.Width, areaSize.Height));
				dc.PushClip(clip);

				// Curve point marker drawing.
				IPointMarker iPointMarker = dataView as IPointMarker;
				Debug.Assert(iPointMarker != null, "iPointMarker != null");
				Drawing pointMarkerDrawing = null;
				if (iPointMarker.PointMarkerVisible)
					pointMarkerDrawing = iPointMarker.PointMarker;
				if (pointMarkerDrawing != null)
				{ // Add Curve point markers.
					foreach (Point pt in points)
					{
						if (isInsideArea(pt, areaSize))
						{
							Drawing marker = pointMarkerDrawing.Clone();
							marker.Freeze();

							PointVisual pointMarker = new PointVisual(marker);
							pointMarker.Transform = new TranslateTransform(pt.X, pt.Y);
							Children.Add(pointMarker);
						}
					}
				}

				// Bezier points.
				Point[] bezierPoints = BezierPoints(points.ToArray());

				if (bezierPoints.Length == 0)
					return; // Nothing to draw

				if (dataView.FillArea && dataView.AreaBrush != null)
				{ // Draw the Curve Area.
					Point firstPoint = points.First();
					// Curve Area figure.
					PathFigure figure = new PathFigure(firstPoint
						, new PathSegment[] { new PolyBezierSegment(bezierPoints, false) }, true);
					// Make the polygon.
					if (dataView.Orientation == Orientation.Horizontal)
					{
						Point bottomRight = new Point(bezierPoints[bezierPoints.Length - 1].X
							, areaSize.Height);
						Point bottomLeft = new Point(firstPoint.X, areaSize.Height);

						figure.Segments.Add(new LineSegment(bottomRight, false));
						figure.Segments.Add(new LineSegment(bottomLeft, false));
					}
					else // dataView.Orientation == Orientation.Vertical
					{
						Point bottomLeft = new Point(0, bezierPoints[bezierPoints.Length - 1].Y);
						Point topLeft = new Point(0, firstPoint.Y);

						figure.Segments.Add(new LineSegment(bottomLeft, false));
						figure.Segments.Add(new LineSegment(topLeft, false));
					}

					// Curve Area geometry.
					PathGeometry geometry = new PathGeometry(new PathFigure[] { figure });
					geometry.Freeze();

					dc.DrawGeometry(dataView.AreaBrush, null, geometry);
				}

				if (dataView.Pen != null)
				{ // Draw the Curve.
					// Curve geometry.
					StreamGeometry geometry = new StreamGeometry();
					using (StreamGeometryContext ctx = geometry.Open())
					{
						ctx.BeginFigure(points.First(), false /* is filled */, false /* is closed */);
						ctx.PolyBezierTo(bezierPoints, true /* is stroked */, true /* is smooth join */);
					}
					geometry.Freeze();

					dc.DrawGeometry(Brushes.Transparent, dataView.Pen, geometry);
				}
			}
		}

		/// <summary>
		/// Get Bezier Points.
		/// </summary>
		/// <param name="points">Knot Bezier spline points.</param>
		/// <returns>The array of points that specify control points and destination points for one 
		/// or more Bezier curves. The number of points == (points.Length - 1) * 3.</returns>
		private static Point[] BezierPoints(Point[] points)
		{
			int n = points.Length - 1;
			if (n < 1)
				return new Point[0];

			// Calculate first Bezier control points
			// Right hand side vector
			double[] r = new double[n];

			// Set right hand side X values
			for (int i = 1; i < n - 1; ++i)
				r[i] = 4 * points[i].X + 2 * points[i + 1].X;
			r[0] = points[0].X + 2 * points[1].X;
			r[n - 1] = (8 * points[n - 1].X + points[n].X) / 2.0;
			// Get first control points X-values
			double[] x = Solve(r);

			// Set right hand side Y values
			for (int i = 1; i < n - 1; ++i)
				r[i] = 4 * points[i].Y + 2 * points[i + 1].Y;
			r[0] = points[0].Y + 2 * points[1].Y;
			r[n - 1] = (8 * points[n - 1].Y + points[n].Y) / 2.0;
			// Get first control points Y-values
			double[] y = Solve(r);

			Point[] bezierPoints = new Point[n * 3];
			for (int i = 0; i < n; ++i)
			{
				// First control point
				bezierPoints[3 * i] = new Point(x[i], y[i]);
				// Second control point
				if (i < n - 1)
					bezierPoints[3 * i + 1] = new Point(2 * points[i + 1].X - x[i + 1], 2 * points[i + 1].Y - y[i + 1]);
				else
					bezierPoints[3 * i + 1] = new Point((points[n].X + x[n - 1]) / 2, (points[n].Y + y[n - 1]) / 2);
				// Bezier knot point
				bezierPoints[3 * i + 2] = points[i + 1];
			}

			return bezierPoints;
		}

		/// <summary>
		/// Solves a tridiagonal system for one of coordinates of first Bezier control point.
		/// </summary>
		/// <param name="rhs">Right hand side vector.</param>
		/// <returns>Solution vector.</returns>
		static double[] Solve(double[] rhs)
		{
			int n = rhs.Length;
			double[] x = new double[n]; // Solution vector.
			double[] tmp = new double[n]; // Temp workspace.

			double b = 2.0;
			x[0] = rhs[0] / b;
			for (int i = 1; i < n; i++) // Decomposition and forward substitution.
			{
				tmp[i] = 1 / b;
				b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
				x[i] = (rhs[i] - x[i - 1]) / b;
			}
			for (int i = 1; i < n; i++)
				x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution.

			return x;
		}

		#region ICurveVisual Members
		/// <inheritdoc />
		public CurvePath GetCurvePath(bool isStroked)
		{
			SampledCurveDataView dataView = DataView as SampledCurveDataView;
			if (dataView == null)
				return new CurvePath();
			IPoint2DSeries pointSeries = dataView.ItemData as IPoint2DSeries;
			if (pointSeries == null)
				return new CurvePath();

			ChartScale hScale = dataView.HorizontalScale, vScale = dataView.VerticalScale;
			if (hScale == null || !hScale.CompatibleWith(dataView.ItemData.GetDimensionType(0))
				|| vScale == null || !vScale.CompatibleWith(dataView.ItemData.GetDimensionType(1)))
				return new CurvePath();

			// Points to draw.
			IEnumerable<Point> points = null;
			if (dataView.Orientation == Orientation.Horizontal)
				points = from pt in pointSeries.Points
						 select new Point(hScale.ToPixels(pt.X), vScale.ToPixels(pt.Y));
			else // Orientation == Orientation.Vertical
				points = from pt in pointSeries.Points
						 select new Point(vScale.ToPixels(pt.Y), hScale.ToPixels(pt.X));
			if (points.Count() == 0)
				return new CurvePath(); // Path is empty

			Point[] bezierPoints = BezierPoints(points.ToArray());
			if (bezierPoints.Length == 0)
				return new CurvePath(); // Path is empty

			return new CurvePath(new PathSegment[] { new PolyBezierSegment(bezierPoints, isStroked) }
				, points.First(), points.Last());
		}
		#endregion ICurveVisual Members
	}
}
